如何扩展第三方模块:复用Mongose实例
本节详细介绍如何通过继承覆写的方式扩展 @nestjs/mongoose 官方模块,解决多租户场景下的连接泄漏问题,实现 Mongoose 连接实例的复用。
创建自定义 MongooseModule
利用 TypeScript 的 extends 继承官方模块,覆写 forRoot 和 forRootAsync 方法:
import { DynamicModule, Module } from '@nestjs/common';
import {
MongooseModule as NestMongooseModule,
} from '@nestjs/mongoose';
@Module({})
export class MongooseModule extends NestMongooseModule {
static forRoot(options): DynamicModule {
// 覆写 forRoot 逻辑
return MongooseCoreModule.forRoot(options);
}
static forRootAsync(options): DynamicModule {
// 覆写 forRootAsync 逻辑
return MongooseCoreModule.forRootAsync(options);
}
}
typescript
创建 MongooseCoreModule
由于官方包没有导出 MongooseCoreModule(index.ts 中只导出了 MongooseModule),需要将核心逻辑复制到本地。
需要同步复制的文件:
mongoose.constants.ts-- 内部常量定义(MONGOOSE_MODULE_OPTIONS等未被导出)mongoose.utils.ts-- 工具方法(handleRetry等未被导出)mongoose-core.module.ts-- 核心连接逻辑
注意:如果觉得复制过程过于复杂,可以直接将整个包的源码复制到项目中修改。这种方式在离线场景下尤其适用。
版本兼容性问题
复制的代码可能遇到版本不匹配的问题。例如本地安装的是 10.0.5,而官方仓库已经是 10.0.6:
# 更新到与源码一致的版本
pnpm install @nestjs/mongoose@10.0.6
bash
连接实例复用的核心实现
1. 定义静态 connections 属性
export class MongooseCoreModule {
private static connections: Record<string, Connection> = {};
}
typescript
2. 覆写 createMongooseConnection
在创建连接前检查缓存:
private async createMongooseConnection(uri: string): Promise<Connection> {
// 如果已有相同 URI 的连接,直接复用
if (MongooseCoreModule.connections[uri]) {
return MongooseCoreModule.connections[uri];
}
// 创建新的 Mongoose 连接
const connection = await mongoose.createConnection(uri).asPromise();
// 缓存到 connections 中
MongooseCoreModule.connections[uri] = connection;
return connection;
}
typescript
3. 应用关闭时清理连接
onApplicationShutdown() {
const { connections } = MongooseCoreModule;
if (connections && Object.keys(connections).length > 0) {
for (const key of Object.keys(connections)) {
const client = connections[key];
if (client && typeof client.close === 'function') {
client.close();
}
}
}
}
typescript
验证连接复用效果
测试步骤
- 启动应用:
pnpm start:dev - 查看 MongoDB 连接数:
db.serverStatus().connections - 多次发起请求,观察连接数变化
验证结果
# 连接到 mongo 容器
docker exec -it mongo mongosh -u root -p 123456
# 查看连接数
db.serverStatus().connections
bash
| 操作 | default 连接数 | default1 连接数 |
|---|---|---|
| 启动后 | 2 (active 1) | 2 (active 1) |
| 请求 default | 3 (active 1) | 不变 |
| 多次请求 default | 不变 | 不变 |
| 请求 default1 | 不变 | 5 (active 2) |
| 多次请求 default1 | 不变 | 不变 |
| 停止应用 | 2 (active 1) | 2 (active 1) |
优化前:每次请求新增连接,连接数持续增长 优化后:相同 URI 复用连接,连接数稳定不变
日志验证
// 添加调试日志
console.log('Connections:', MongooseCoreModule.connections);
// 第一次请求: Connections: {}
// 第二次请求: Connections: { 'mongodb://...': Connection {...} }
// 后续请求: 复用已有 Connection,不进入创建逻辑
typescript
Mongoose 连接池说明
MongoDB 驱动默认使用连接池,初始连接时会创建多个连接(如 current: 5),这是正常行为。连接池的大小可以通过 MongooseModuleOptions 中的配置调整:
{
uri: 'mongodb://...',
// 连接池配置
connectionFactory: (connection) => connection,
}
typescript
关键验证点是:同一 URI 在多次请求后 不再创建新的连接,而不是连接池的初始连接数不变。
本节总结
- 通过
extends继承官方模块,只需覆写forRoot/forRootAsync和连接创建逻辑 - 官方未导出的常量和工具方法需要本地创建副本
- 使用
static connections以 URI 为 key 缓存连接实例 - 在
createMongooseConnection中优先检查缓存,实现连接复用 - 在
onApplicationShutdown中遍历并关闭所有连接,释放资源 - 优化后连接数保持稳定,不再随请求数增长
- 由于
forRoot和forRootAsync共用createMongooseConnection,修改一处即同时生效
↑